<?php

// vim: expandtab shiftwidth=2 softtabstop=2 tabstop=2

/**
 * Class for image manipulations
 *
 * PHP version 5
 *
 * @category  Classes
 * @package   mPack
 * @author    Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: class_mimage.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */

/**
 * Class for image manipulations
 *
 * Scaling, resizing, cropping, applying filters and many others.
 *
 * Error codes:
 * 140 - InvalidArgumentException : Cannot read "%s"
 * 141 - InvalidArgumentException : Invalid filter: "%s".
 * 142 - InvalidArgumentException : Attribute "%s" is read only.
 * 143 - InvalidArgumentException : Both values: $max_width and $max_height cannot be false
 * 144 - InvalidArgumentException : Incorrect quant of elements in "$padding" parameter.
 * 145 - InvalidArgumentException : Attribute "%s" doesn't exists.
 * 100 - LogicException           : Image no loaded - call mImage::open() first.
 * 250 - UnexpectedValueException : Specified image (%s) has dimensions different then %dx%d.
 * 200 - RuntimeException         : Cannot write to "%s" directory.
 * 201 - RuntimeException         : File "%s" already exists.
 * 120 - BadMethodCallException   : Incorrect function imagerotate.
 *
 * @category  Classes
 * @package   mPack
 * @author    Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: class_mimage.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */
class mImage {

  /**
   * Constant - default output format.
   */
  const FORMAT = 'png';
  /**
   * Constant - default jpeg quality.
   */
  const QUALITY = 75;

  /**
   * Constant - max size of image. Used when in {scale,grow,shrink}Prop
   * as $max_{width,height} is false.
   */
  const MAX_SIZE = 40960;

  /**
   * Properties of image
   *
   * Have to be read only, so we keep it in internal array and
   * access to them is via __set() and __get() methods
   *
   * @var array
   * @access protected
   */
  protected $properties = array(
    'width'     => null,
    'height'    => null,
    'truecolor' => null
  );

  /**
   * Image descriptor (handler)
   *
   * @var object
   * @access protected
   */
  protected $body = null;

  /**
   * Available filters
   *
   * List is initialized from constructor, because filters are available
   * only if php use it's internal gd.
   *
   * @var array
   * @access protected
   * @static
   */
  protected static $filters;

  /**
   * Image formats handled by this class.
   *
   * This array maps possible formats to assigned with them functions from
   * image extension.
   *
   * @var array
   * @access protected
   * @static
   */
  protected static $formats = array(
    'jpg'  => 'imagejpeg',
    'jpeg' => 'imagejpeg',
    'gif'  => 'imagegif',
    'png'  => 'imagepng'
  );

  /**
   * Constructor.
   *
   * If filename in argument is specfified, it opens file and read them.
   * Assign available filters too (if any).
   *
   * Params description: {@link mImage::open()}
   *
   * @param string  $fname
   * @param mixed   $width
   * @param mixed   $height
   * @param boolean $exact
   *
   * @access public
   */
  public function __construct($fname=null, $width=null, $height=null,
                              $exact=true)
  {
    // these constants aren't compiled in if php use external gd.
    // In this case, we cant put here these constant, only
    // if imagefilter() function exists (it means: when php use bundled
    // version of gd)
    if (function_exists('imagefilter') && !self::$filters) {
      self::$filters = array(
        'negate'      => IMG_FILTER_NEGATE,
        'grayscale'   => IMG_FILTER_GRAYSCALE,
        'brightness'  => IMG_FILTER_BRIGHTNESS,
        'contrast'    => IMG_FILTER_CONTRAST,
        'colorize'    => IMG_FILTER_COLORIZE,
        'edge'        => IMG_FILTER_EDGEDETECT,
        'emboss'      => IMG_FILTER_EMBOSS,
        'gaussian'    => IMG_FILTER_GAUSSIAN_BLUR,
        'blur'        => IMG_FILTER_SELECTIVE_BLUR,
        'sketchy'     => IMG_FILTER_MEAN_REMOVAL,
        'smooth'      => IMG_FILTER_SMOOTH
      );
    }

    if (!is_null($fname)) {
      $this->open($fname, $width, $height, $exact);
    }
  }

  /**
   * Destructor
   *
   * Destroy opened image
   *
   * @access public
   */
  public function __destruct()
  {
    if (is_object($this->body)) {
      imagedestroy($this->body);
    }
  }

  /**
   * Read file and assign properties.
   *
   * If $width and $height are not null, and $exact:
   * - is true: if image dimensions are other then $width x $height, raise
   *   UnexpectedValueException
   * - is false: propportional scale image to specified dimensions.
   *
   * $width or $height can be false ({@link mImage::scaleProp() more}).
   *
   * @param string  $fname  filename to read
   * @param mixed   $width
   * @param mixed   $height
   * @param boolean $exact
   *
   * @return mixed
   * @throws InvalidArgumentException
   * @throws UnexpectedValueException
   * @access public
   */
  public function open($fname, $width=null, $height=null, $exact=true)
  {
    $fname = realpath($fname);
    if (empty($fname) || !file_exists($fname) || !is_readable($fname)) {
      throw new InvalidArgumentException(sprintf('Cannot read "%s".', $fname), 140);
    }

    $dst = imagecreatefromstring(file_get_contents($fname));
    $this->swap($dst);

    if (!is_null($width) && !is_null($height)) {
      if (false === $width && false === $height) {
        throw new InvalidArgumentException('Both values: $max_width and $max_height ' .
          'cannot be false', 143);
      }

      if ($exact) { // if image has other dimensions then specified
        if (
          ( false !== $width  && $width  != $this->width  ) ||
          ( false !== $height && $height != $this->height )
           ) {

          throw new UnexpectedValueException(sprintf('Specified image (%s) has ' .
            'dimensions different then %dx%d.',

            $fname,
            $width,
            $height
          ), 250);
        }
      } else {
        return $this->scaleProp($width, $height, 'ffffff', false, 'c', 0);
      }
    }

    return true;
  }

  /**
   * Send image to standard output.
   *
   * @param string  $format       possible values in self::$formats
   * @param integer $quality      quality of output jpeg
   * @param boolean $sendHeaders  if true, send content-type header
   *
   * @access public
   */
  public function show($format=null, $quality=null, $sendHeaders=true)
  {
    $this->isInitialized();

    $format = $this->checkFormat($format);

    if ($sendHeaders) {
      header('Content-type: image/' . $format);
    }

    if (is_null($quality)) {
      $quality = self::QUALITY;
    }

    $fun = self::$formats[$format];
    $fun($this->body, '', $quality);
  }

  /**
   * Save image to given file.
   *
   * @param string $fname filename
   * @param string $format possible values in self::$formats
   * @param integer $quality quality of output jpeg
   * @param boolean $overwrite if true, it overwrite file if it exists
   *
   * @throws RuntimeException
   * @access public
   */
  public function saveToFile($fname, $format=null, $quality=null, $overwrite=false)
  {
    $this->isInitialized();

    $pathinfo = pathinfo($fname);

    if (is_null($format)) {
      $format = $pathinfo['extension'];
    }
    $format = $this->checkFormat(strtolower($format));

    if ('' == $pathinfo['dirname']) {
      $pathinfo['dirname'] = '.';
    }
    $pathinfo['dirname'] = realpath($pathinfo['dirname']);
    $fullpath = $pathinfo['dirname'] . DIRECTORY_SEPARATOR . $pathinfo['basename'];

    if (!is_writeable($pathinfo['dirname'])) {
      throw new RuntimeException(sprintf('Cannot write to "%s" directory.', $pathinfo['dirname']), 200);
    }
    if (file_exists($fullpath)) {
      if ($overwrite) {
        unlink($fullpath);
      } else {
        throw new RuntimeException(sprintf('File "%s" already exists.', $fullpath), 201);
      }
    }

    if (is_null($quality)) {
      $quality = self::QUALITY;
    }

    $fun = self::$formats[$format];
    $fun($this->body, $fname, $quality);
  }

  /**
   * Return image as an string.
   *
   * @param string  $format  possible alues in self::$formats
   * @param integer $quality quality of output jpeg
   *
   * @return string
   * @access public
   */
  public function toString($format=null, $quality=null)
  {
    $format = $this->checkFormat($format);

    ob_start();
    $fun = self::$formats[$format];
    $fun($this->body, '', $quality);
    return ob_get_flush();
  }

  /**
   * Proportional scaling of image
   *
   * If $fill == true, image dimensions has been equal to $max_{width,height}.
   * Free space between scaled image and edge of image will be filled by
   * $bgcolor.
   * If $fill == false, image dimensions will be equal to scaled image, not
   * bigger then $max_{width,height}.
   *
   * If $padding is specified, image will be scaled down to be equal
   * or smaller then $max_width-$padding and $max_height-$padding, and free
   * space will be filled by $bgcolor.
   * $padding can be either an integer (all padding are equal) or array:
   * - 2 elements for top/bottom and left/right paddings;
   * - 4 elemnts for top, tight, bottm and left values of padding
   * Scaled image wil be 'placed' into selected (in $position) place of
   * output image.
   *
   * $max_width or $max_height (but no both) can be false. In this case image
   * will be scaled only for second, non-false dimension.
   *
   * Return false if fail.
   *
   * @param mixed   $max_width
   * @param mixed   $max_height
   * @param string  $bgcolor
   * @param boolean $fill
   * @param string  $position
   * @param mixed   $padding
   *
   * @return boolean
   * @throws InvalidArgumentException
   * @access public
   */
  public function scaleProp($max_width, $max_height, $bgcolor='ffffff',
                $fill=true, $position='c', $padding=0)
  {
    $this->isInitialized();

    if ($max_width === false && $max_height === false) {
      throw new InvalidArgumentException('Both values: $max_width and $max_height cannot be false', 143);
    }
    if ($max_width  === false) {
      $max_width  = self::MAX_SIZE;
      $fill     = false;
    }
    if ($max_height === false) {
      $max_height = self::MAX_SIZE;
      $fill     = false;
    }

    if (!is_array($padding)) {
      $padding = array_fill(0, 4, $padding);
    } elseif (2 == count($padding)) {
      $padding = array($padding[0], $padding[1], $padding[0], $padding[1]);
    } elseif (4 != count($padding)) {
      throw new InvalidArgumentException('Incorrect quant of elements in "$padding" parameter.', 144);
    }

    $width  = $max_width  - ($padding[1] + $padding[3]);
    $height = $max_height - ($padding[0] + $padding[2]);

    list($th_width, $th_height) = $this->calculateSize($width, $height);

    if ($fill) {
      $dst_width  = $max_width;
      $dst_height = $max_height;

      $pos = $this->calculatePosition($width, $height, $position);
    } else {
      $dst_width  = $th_width  + ($padding[1] + $padding[3]);
      $dst_height = $th_height + ($padding[0] + $padding[2]);

      $pos = $this->calculatePosition($th_width, $th_height, $position);
    }

    // initializing destination image
    $dst = $this->newImage($dst_width, $dst_height);

    imagefill($dst, 0, 0, $this->color($bgcolor)); //set background color
    $test = imagecopyresampled($dst, $this->body,
      $pos[0]+$padding[3], $pos[1]+$padding[0],
      $pos[2], $pos[3],
      $th_width, $th_height,
      $this->properties['width'], $this->properties['height']
    );

    if ($test) {
      return $this->swap($dst);
    } else {
      return false;
    }
  }

  /**
   * Non proportional scaling
   *
   * If $padding is specified, image will be scaled down to be equal
   * or smaller then $max_width-$padding and $max_height-$padding, and free
   * space will be filled by $bgcolor.
   * $padding can be either an integer (all padding are equal) or array of
   * top, right, bottom, and left padding value.
   *
   * Return false if fail
   *
   * @param integer $width   width of image
   * @param integer $height  height of image
   * @param mixed   $padding
   * @param string  $bgcolor background color
   *
   * @return boolean
   * @access public
   */
  public function scaleNonProp($width, $height, $padding=0, $bgcolor='ffffff')
  {
    $this->isInitialized();

    $dst = $this->newImage($width, $height);
    if ($padding) {
      imagefill($dst, 0, 0, $this->color($bgcolor));
    }
    if (!is_array($padding)) {
      $padding = array_fill(0, 4, $padding);
    }

    $test = imagecopyresampled($dst, $this->body,
        $padding[3], $padding[0], 0, 0,
        $width - ($padding[1] + $padding[3]),
        $height - ($padding[0] + $padding[2]),
        $this->properties['width'], $this->properties['height']
    );

    if ($test) {
      return $this->swap($dst);
    } else {
      return false;
    }
  }

  /**
   * Grow an image (proportional)
   *
   * Grow image if is smaller then $max_width & $max_height. Return true if
   * bigger and leave image without modifying it.
   * Use mImage::scaleProp()
   *
   * $max_width or $max_height (but no both) can be false. In this case image
   * will be scaled only for second, non-false dimension.
   *
   * Params description: {@link mImage::scaleProp()}
   *
   * @param integer $max_width
   * @param integer $max_height
   * @param string  $bgcolor
   * @param boolean $fill
   * @param string  $position
   * @param mixed   $padding
   *
   * @return boolean
   * @access public
   */
  public function growProp($max_width, $max_height, $bgcolor='ffffff',
               $fill=true, $position='c', $padding=0)
  {
    if ($max_width === false && $max_height === false) {
      throw new InvalidArgumentException('Both values: $max_width and $max_height cannot be false', 143);
    }
    if (
        ($max_width  !== false && $this->properties['width']  <= $max_width)  ||
        ($max_height !== false && $this->properties['height'] <= $max_height)
       ) {
      return $this->scaleProp($max_width, $max_height, $bgcolor,
                   $fill, $position, $padding);
     } else {
       return true;
     }
  }

  /**
   * Grow an image (non-proportional)
   *
   * Grow image if is smaller then $max_width & $max_height. Return true if
   * bigger and leave image non touched.
   * Use {@link mImage::scaleNonProp()}
   *
   * Params description: {@link mImage::scaleProp()}
   *
   * @param integer $max_width
   * @param integer $max_height
   * @param string  $bgcolor
   * @param boolean $fill
   * @param string  $position
   * @param mixed   $padding
   *
   * @return boolean
   * @access public
   */
  public function growNonProp($max_width, $max_height,
                $padding=0, $bgcolor='ffffff')
  {
    if ($this->properties['width'] <= $max_width ||
        $this->properties['height'] <= $max_height) {
      return $this->scaleNonProp($max_width, $max_height, $bgcolor,
                    $fill, $position, $padding);
     } else {
       return true;
     }
  }

  /**
   * Shrink an image (proportional)
   *
   * Shrinks image if is bigger then $min_width & $min_height. Return true if
   * smaller and leave image non touched.
   * Use mImage::scaleProp()
   *
   * Params description: {@link mImage::scaleProp()}
   *
   * @param integer $min_width
   * @param integer $min_height
   * @param string  $bgcolor
   * @param boolean $fill
   * @param string  $position
   * @param mixed   $padding
   *
   * @return boolean
   * @access public
   */
  public function shrinkProp($min_width, $min_height, $bgcolor='ffffff',
                 $fill=true, $position='c', $padding=0)
  {
    if ($min_width === false && $min_height === false) {
      throw new InvalidArgumentException('Both values: $min_width and $min_height cannot be false', 143);
    }
    if (
        ($min_width  !== false && $this->properties['width']  >= $min_width)  ||
        ($min_height !== false && $this->properties['height'] >= $min_height)
       ) {
      return $this->scaleProp($min_width, $min_height, $bgcolor,
                   $fill, $position, $padding);
     } else {
       return true;
     }
  }

  /**
   * Shrink an image (non-proportional)
   *
   * Shrinks image if is bigger then $min_width & $min_height. Return true if
   * smaller and leave image non touched.
   * Use mImage::scaleNonProp()
   *
   * Params description: {@link mImage::scaleProp()}
   *
   * @param integer $min_width
   * @param integer $min_height
   * @param string  $bgcolor
   * @param boolean $fill
   * @param string  $position
   * @param mixed   $padding
   *
   * @return boolean
   * @access public
   */
  public function shrinkNonProp($min_width, $min_height,
                  $padding=0, $bgcolor='ffffff')
  {
    if ($this->properties['width'] >= $min_width ||
        $this->properties['height'] >= $min_height) {
      return $this->scaleNonProp($min_width, $min_height,
                    $padding, $bgcolor);
    } else {
      return true;
    }
  }

  /**
   * Cropping of an image, method 1.
   *
   * As parameters are given: x and y coordinats of top left corner
   * cropped area, and width and height of it, or 4-elements array
   * with the same data as $x.
   *
   * @param integer $x
   * @param integer $y
   * @param integer $h
   * @param integer $w
   *
   * @return boolean
   * @access public
   */
  public function crop1($x, $y=null, $h=null, $w=null)
  {
    $this->isInitialized();

    if (is_array($x)) {
      $y = $x[1];
      $h = $x[2];
      $w = $x[3];
      $x = $x[0];
    }

    $dst = $this->newImage($h, $w);

    $test = imagecopy($dst, $this->body, 0, 0, $x, $y, $h, $w);
    if ($test) {
      return $this->swap($dst);
    } else {
      return false;
    }
  }

  /**
   * Cropping of an image, method 2.
   *
   * As parameters are given: top, right, bottom and left coordinates of
   * image, or 4-elements array with the same data as $t.
   *
   * @param integer $t
   * @param integer $r
   * @param integer $b
   * @param integer $l
   *
   * @return boolean
   * @access public
   */
  public function crop2($t, $r=null, $b=null, $l=null)
  {
    if (is_array($t)) {
      $r = $t[1];
      $b = $t[2];
      $l = $t[3];
      $t = $t[0];
    }

    return $this->crop1($t, $l, $r - $l, $b - $t);
  }

  /**
   * Apply filter to an image
   *
   * @param string $filter   filter name (available filters from self::$filters)
   * @param string $filter,... {@link http://php.net/imagefilter description}
   *
   * @return boolean
   * @throws InvalidArgumentException if invalid filter name
   * @access public
   */
  public function filter($filter)
  {
    $this->isInitialized();

    if (!in_array($filter, self::$filters)) {
      throw new InvalidArgumentException(sprintf('Invalid filter: "%s".', $filter), 141);
    }

    //not tested yet - if fail, remove line below
    $argv = func_get_args();
    array_shift($argv);
    return call_user_func_array('imagefilter',
      array_merge(array($this->body, self::$filters[$filter]),
      $argv)
    );

    // Workaround: i don't know why, but imagefilter() throw some fatal
    // error if there is all 4 possible arguments, so i must give him
    // only that, which are required at call time (depends of filter)
    $argc = func_num_args();
    $argv = func_get_args();
    switch ($argc) {
      case 1: return imagefilter($this->body, self::$filters[$filter]);
      case 2: return imagefilter($this->body, self::$filters[$filter], $argv[1]);
      case 3: return imagefilter($this->body, self::$filters[$filter], $argv[1], $argv[2]);
      default:
        return imagefilter($this->body, self::$filters[$filter], $argv[1], $argv[2], $argv[3]);
    }
  }

  /**
   * Apply negate filter to an image
   *
   * @return boolean
   * @access public
   */
  public function filterNegate()
  {
    return $this->filter('negate');
  }

  /**
   * Apply grayscale filter to an image
   *
   * @return boolean
   * @access public
   */
  public function filterGrayscale()
  {
    return $this->filter('grayscale');
  }

  /**
   * Apply brightness filter to an image
   *
   * @param integer $brightness
   *
   * @return boolean
   * @access public
   */
  public function filterBrightness($value)
  {
    return $this->filter('brightness', $value);
  }

  /**
   * Apply contrast filter to an image
   *
   * @param integer $contrast
   *
   * @return boolean
   * @access public
   */
  public function filterContrast($value)
  {
    return $this->filter('contrast', $value);
  }

  /**
   * Apply colorize filter to an image
   *
   * Color can be as html notation, like 'ffffff' (white) or '000000'
   * (black), either too three arguments, each one power of red ($r),
   * green ($g) or blue ($b).
   *
   * @param mixed   $r
   * @param integer $g
   * @param integer $b
   *
   * @return boolean
   * @access public
   */
  public function filterColorize($r, $g=null, $b=null)
  {
    if (is_null($g) || is_null($b)) {
      $rgb = $this->hex2rgb($r);
      if (false === $rgb) {
        return false;
      }
      list($r, $g, $b) = $rgb;
    }
    return $this->filter('colorize', $r, $g, $b);
  }

  /**
   * Apply edge filter to an image
   *
   * @return boolean
   * @access public
   */
  public function filterEdge()
  {
    return $this->filter('edge');
  }

  /**
   * Apply emboss filter to an image
   *
   * @return boolean
   * @access public
   */
  public function filterEmboss()
  {
    return $this->filter('emboss');
  }
  /**
   * Apply gaussian blur filter to an image
   *
   * @return boolean
   * @access public
   */
  public function filterGaussian()
  {
    return $this->filter('gaussian');
  }
  /**
   * Apply blur filter to an image
   *
   * @return boolean
   * @access public
   */
  public function filterBlur()
  {
    return $this->filter('blur');
  }
  /**
   * Apply sketchy filter to an image
   *
   * @return boolean
   * @access public
   */
  public function filterSketchy()
  {
    return $this->filter('sketchy');
  }
  /**
   * Apply smooth filter to an image
   *
   * @param integer $value smoothness
   *
   * @return boolean
   * @access public
   */
  public function filterSmooth($value)
  {
    return $this->filter('smooth', $value);
  }
  /**
   * Rotate image for given angle
   *
   * @param integer $angle
   * @param string  $bgcolor background color
   * @param boolean $ignore_transparent {@link http://php.net/imagerotate}
   *
   * @access public
   */
  public function rotate($angle, $bgColor='ffffff', $ignoreTransparent=false)
  {
    $this->isInitialized();

    if (!function_exists('imagerotate')) {
      throw new BadMethodCallException('Incorrect function imagerotate.', 120);
    }

    $angle = (float)$angle;
    $color = $this->color($bgColor);
    $this->body = imagerotate($this->body, $angle, $color, $ignoreTransparent);
  }

  /**
   * Flip image in vertical.
   *
   * @return bool
   * @access public
   */
  public function flipVertical()
  {
    return $this->flip('vertical');
  }

  /**
   * Flip image in horizontal.
   *
   * @return bool
   * @access public
   */
  public function flipHorizontal()
  {
    return $this->flip('horizontal');
  }

  /**
   * Flip image in both directions.
   *
   * @return bool
   * @access public
   */
  public function flipBoth()
  {
    return $this->flip('both');
  }

  /**
   * Flips image in $direction direction.
   *
   * $direction can be:
   *   - vertical
   *   - horizontal
   *   - both
   *
   * @param string $direction
   * @return bool
   */
  public function flip($direction='both')
  {
    $dst = $this->newImage();

    switch( $direction ) {
      case 'horizontal':
        for($i=0; $i<$this->properties['height']; ++$i) {
          imagecopy($dst, $this->body, 0, $this->properties['height']-$i-1, 0, $i, $this->properties['width'], 1);
        }
      break;

      case 'vertical':
        for($i=0; $i<$this->properties['width']; ++$i) {
          imagecopy($dst, $this->body, $this->properties['width']-$i-1, 0, $i, 0, 1, $this->properties['height']);
        }
      break;

      case 'both':
        for($i=0; $i<$this->properties['width']; ++$i) {
          imagecopy($dst, $this->body, $this->properties['width']-$i-1, 0, $i, 0, 1, $this->properties['height']);
        }

        $rowBuffer = $this->newImage(null, 1, true);
        for($i=0 ; $i<($this->properties['height']/2) ; ++$i ) {
          imagecopy($rowBuffer, $dst  , 0, 0, 0, $this->properties['height']-$i-1, $this->properties['width'], 1);
          imagecopy($dst, $dst, 0, $this->properties['height']-$i-1, 0, $i, $this->properties['width'], 1);
          imagecopy($dst, $rowBuffer, 0, $i, 0, 0, $this->properties['width'], 1);
        }

        imagedestroy($rowBuffer);
      break;
    }

    return $this->swap($dst);
  }

  /**
   * Apply an border to image
   *
   * Border can be solid, or pattern ({@link http://php.net/imagesetstyle}).
   * Each of borders can be other color/style, and thickness.
   *
   * If $thickness is an integer, it means is one value (thickness) for
   * all borders. $thickness can be an array too, and it must have 4
   * elements, one per one border.
   *
   * Colors/style can be set one for all edges (only $colT set), different
   * for top/bottom and left/right edges (set $colT and $colR,
   * and different for any of edge (all 4 $col* must be set).
   *
   * Colors can be as hex value in html notation, or result of
   * imagecolorallocate() ({@link http://php.net/imagecolorallocate}).
   *
   * If You want to set pattern for edges, You must set proper arrays
   * ({@link http://php.net/imagesetstyle}).
   *
   * @param mixed $thickness
   * @param mixed $colT
   * @param mixed $colR
   * @param mixed $colB
   * @param mixed $colL
   *
   * @return boolean
   * @throws InvalidArgumentException
   * @access public
   */
  public function border($thickness, $colT, $colR=null, $colB=null, $colL=null)
  {
    if (!is_array($thickness)) { //all borders
      $thickness = array_fill(0, 4, $thickness);
    } elseif (2 == count($thickness)) { //top and bottom, left and right
      $tmp = $thickness;
      $thickness = array($tmp[0], $tmp[1], $tmp[0], $tmp[1]);
      unset($tmp);
    } elseif (4 != count($thickness)) { //four different
      throw new InvalidArgumentException('Incorrect quant of elements in "$thickness" parameter.', 144);
    }

    if (is_null($colR)) { //all borders have one style
      $colR = $colB = $colL = $colT;
    } elseif (is_null($colB)) { //borders top and bottom have one style, left and right - second
      $colB = $colT;
      $colL = $colR;
    }
    $col = array($colT, $colR, $colB, $colL);

    $this->drawLine( //top
      array(0, $thickness[0]/2),
      array($this->properties['width'], $thickness[0]/2),
      $colT, $thickness[0]);
    $this->drawLine( //right
      array($this->properties['width'] - ($thickness[1]/2), 0),
      array($this->properties['width'] - ($thickness[1]/2),
        $this->properties['height']),
      $colR, $thickness[1]);
    $this->drawLine( //left
      array($this->properties['width'],
        $this->properties['height'] - ($thickness[2]/2) ),
      array(0, $this->properties['height'] - ($thickness[2]/2) ),
      $colB, $thickness[2]);
    $this->drawLine( //bottom
      array($thickness[3]/2, $this->properties['height']),
      array($thickness[3]/2, 0),
      $colL, $thickness[3]);

    return true;
  }

  /**
   * Add layer on top of image.
   *
   * If $layer is string, it suppose to be file name, which be loaded and
   * merged with current image.
   * $layer can also be an image object ({@link mImage::get()}).
   *
   * Can be used to adding some blenda do thumbnails, for example.
   *
   * @param mixed   $layer
   * @param integer $alpha opacity of new layer
   *
   * @return boolean
   * @access public
   */
  public function addLayer($layer, $alpha=100)
  {
    if (is_string($layer) && is_file($layer)) {
      $i = new mImage($layer);
      $layer = $i->get();
    }
    if (!$layer) {
      return false;
    }

    return imagecopymerge($this->body, $layer, 0, 0, 0, 0, imagesx($layer),
        imagesy($layer), $alpha);
  }

  /**
   * Allocate color.
   *
   * Can be used when adding borders etc.
   * {@link http://php.net/imagecolorallocate}
   *
   * @param string   $hex color in html notation
   * @param resource $img if not null, used by imagecolorallocate
   *
   * @return integer
   * @access public
   */
  public function color($hex, &$img=null)
  {
    list($r, $g, $b) = $this->hex2rgb($hex);
    if (is_null($img)) {
      return imagecolorallocate($this->body, $r, $g, $b);
    } else {
      return imagecolorallocate($img, $r, $g, $b);
    }
  }

  /**
   * Return current image object
   *
   * @return resource
   * @access public
   */
  public function get()
  {
    return clone $this->body;
  }

  /**
   * Draw a line on image
   *
   * Currently used only for drawig borders/
   * $begin and $end holds coordinates of begin and end points of line.
   * $color can be:
   * - integer - if line have to be solid
   * - array - if line have to be an pattern ({@link http://php.net/imagesestyle})
   *
   * @param array   $begin
   * @param array   $end
   * @param mixed   $color
   * @param integer $thickness
   *
   * @return boolean
   * @access public
   */
  protected function drawLine(array $begin, array $end, $color, $thickness)
  {
    imagesetthickness($this->body, $thickness);

    if (!is_array($color)) { //solid
      if (is_string($color)) {
        $color = $this->color($color);
      }
      imageline($this->body,
          $begin[0], $begin[1],
          $end[0], $end[1],
          $color);
    } else { //pattern
      imagesetstyle($this->body, $color);
      imageline($this->body,
          $begin[0], $begin[1],
          $end[0], $end[1],
          IMG_COLOR_STYLED);
    }
    return true;
  }

  /**
   * Checks for image that was loaded
   *
   * If image wasn't loaded, and $exc == true, an exception is raised.
   * If $exc == false, isInitialized() return false.
   *
   * @param boolean $silent if true, an exception is raised when image wasn't loaded
   *
   * @return boolean
   * @throws LogicException
   * @access protected
   */
  protected function isInitialized($silent=false)
  {
    if (is_null($this->body)) {
      if ($silent) {
        return false;
      } else {
        throw new LogicException('Image no loaded - call mImage::open() first.', 100);
      }
    }
    return true;
  }

  /**
   * Converts html notation of color to rgb array
   *
   * Return false or 3 element array with rgb data.
   *
   * @param string $hex color in html notation
   *
   * @return mixed
   * @access protected
   */
  protected function hex2rgb($hex)
  {
    if (6 != strlen($hex)) {
      return false;
    }

    $data = str_split($hex, 2);
    $data[0] = hexdec($data[0]);
    $data[1] = hexdec($data[1]);
    $data[2] = hexdec($data[2]);
    return $data;
  }

  /**
   * Creates new image object
   *
   * If $width or $height are null, these values are got from
   * $this->{width,height}.
   *
   * @param integer $width
   * @param integer $height
   * @param boolean $truecolor
   *
   * @return object
   * @access protected
   */
  protected function newImage($width=null, $height=null, $truecolor=null)
  {
    //docelowa szerokosc
    if (is_null($width)) {
      $width = $this->properties['width'];
    }
    //docelowa wysokosc
    if (is_null($height)) {
      $height = $this->properties['height'];
    }
    if (is_null($truecolor)) {
      $truecolor = $this->properties['truecolor'];
    }

    if ($truecolor) {
      return imagecreatetruecolor($width, $height);
    } else {
      return imagecreate($width, $height);
    }
  }

  /**
   * Check for correct image format
   *
   * If format is incorrect, it return default format used by mImage class.
   * See: {@link mImage::FORMAT}, {@link mImage::$formats}
   *
   * @param string $format
   *
   * @return string
   * @access protected
   */
  protected function checkFormat($format=null)
  {
    if (is_null($format) || !array_key_exists($format, self::$formats)) {
      $format = self::FORMAT;
    }
    if ('jpg' == $format) {
      $format = 'jpeg';
    }
    return $format;
  }

  /**
   * Calculate proportional size of scaled image
   *
   * @param integer $max_width
   * @param integer $max_height
   *
   * @return array
   * @access protected
   */
  protected function calculateSize(&$max_width, &$max_height)
  {
    $div_width  = (double)($this->properties['width']  / $max_width);
    $div_height = (double)($this->properties['height'] / $max_height);

    $div = max($div_width, $div_height);

    $ret = array();
    $ret[] = (double)($this->properties['width']  / $div);
    $ret[] = (double)($this->properties['height'] / $div);
    $ret[] = &$div;

    return $ret;
  }

  /**
  * Calculate position of scaled image
  *
  * (coordinates of left top point and
  *
  * @param integer $max_width
  * @param integer $max_height
  * @param string  $position
  *
  * @return array
  * @access protected
  */
  protected function calculatePosition(&$max_width, &$max_height, $position)
  {
    //'c', 'lt', 'ct', 'rt', 'lc', 'rc', 'lb', 'cb', 'rb'
    list($th_w, $th_h) = $this->calculateSize($max_width, $max_height);
    $ret = array(0, 0, 0, 0);
    switch ($position)
    {
      case 'lt':
      break;
      case 'ct':
        $ret[0] = ($max_width-$th_w)/2;
      break;
      case 'rt':
        $ret[0] = $max_width-$th_w;
      break;
      case 'lc':
        $ret[1] = ($max_height-$th_h)/2;
      break;
      case 'rc':
        $ret[0] = $max_width-$th_w;
        $ret[1] = ($max_height-$th_h)/2;
      break;
      case 'lb':
        $ret[1] = $max_height-$th_h;
      break;
      case 'cb':
        $ret[0] = ($max_width-$th_w)/2;
        $ret[1] = $max_height-$th_h;
      break;
      case 'rb':
        $ret[0] = $max_width-$th_w;
        $ret[1] = $max_height-$th_h;
      break;
      default:
        if ($th_w > $th_h) {
          $ret[1] = ($max_height - $th_h) / 2;
        } else {
          $ret[0] = ($max_width - $th_w) / 2;
        }
    }

    return $ret;
  }

  /**
   * Put given argument as content of mImage::$body
   *
   * @param object $dst
   *
   * @return boolean
   * @access protected
   */
  protected function swap(&$dst)
  {
    if (is_resource($dst)) {
      if (!is_null($this->body)) {
        imagedestroy($this->body);
      }
      $this->body = $dst;

      $this->properties['width']      = imagesx($this->body);
      $this->properties['height']     = imagesy($this->body);
      $this->properties['truecolor']  = imageistruecolor($this->body);

      return true;
    } else {
      return false;
    }
  }

  /**
   * Overloaded setter
   *
   * Use mImage::$properties as store container.
   *
   * @param string $k name of property
   * @param mixed  $v value of property
   *
   * @throws InvalidArgumentException
   * @access public
   */
  public function __set($k, $v)
  {
    if (array_key_exists($k, $this->properties)) {
      throw new InvalidArgumentException(sprintf('Attribute "%s" is read only.', $k), 142);
    } else {
      throw new InvalidArgumentException(sprintf('Attribute "%s" doesn\'t exists.', $k), 145);
    }
  }

  /**
   * Overloaded getter
   *
   * Use mImage::$properties as store container.
   *
   * @param string $k name of property
   *
   * @return mixed
   * @throws InvalidArgumentException
   * @access public
   */
  public function __get($k)
  {
    if (array_key_exists($k, $this->properties)) {
      return $this->properties[$k];
    } else {
      throw new InvalidArgumentException(sprintf('Attribute "%s" doesn\'t exists.', $k), 145);
    }
  }

  /**
   * Overloaded isset()
   *
   * Use mImage::$properties as store container.
   *
   * @param string $k name of property
   *
   * @return boolean
   * @access public
   */
  public function __isset($k)
  {
    if (array_key_exists($k, $this->properties) &&
        !is_null($this->properties[$k])) {
      return true;
    } else {
      return false;
    }
  }

  /**
   * Return array of available filters
   *
   * Can be usefull when we want to show which filters are available
   * at runtime.
   *
   * @return array
   */
  public function getFilters()
  {
    return self::$filters;
  }
}

?>
